#include "alsarenderer.h"
#include "audioformat.h"
#include "audioframe.h"

#include <stdexcept>
#include <iostream>
#include <algorithm>
#include <assert.h>
#include <vector>

using namespace std;

static const unsigned int FRAME_BUFFER_SIZE = 5;

class AlsaFrame
{
public:
    AlsaFrame(byte* pFrameData, unsigned int frameSize, double pts)
    : m_pFrameData(0)
    , m_TotalBytes(frameSize)
    , m_ProcessedBytes(0)
    , m_Pts(pts)
    {
        m_pFrameData = new byte[frameSize];
        memcpy(m_pFrameData, pFrameData, frameSize);
    }

    ~AlsaFrame()
    {
        delete[] m_pFrameData;
    }

    byte*   m_pFrameData;
    int     m_TotalBytes;
    int     m_ProcessedBytes;
    double  m_Pts;
};

static void alsaCallBack(snd_async_handler_t* pPcmCallback)
{
    AlsaRenderer* pAlsaRenderer = static_cast<AlsaRenderer*>(snd_async_handler_get_callback_private(pPcmCallback));
    pAlsaRenderer->playFrameData();
}

AlsaRenderer::AlsaRenderer()
: AudioRenderer()
, m_pAudioDevice(NULL)
, m_BufferSize(0)
, m_PeriodSize(512)
, m_pAlsaCallBack(0)
, m_FramesPerPacket(0)
, m_FrameSize(0)
, m_LastPts(0.0)
{
    string deviceName = "default";

    throwOnError(snd_pcm_open(&m_pAudioDevice, deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0), "Error opening PCM device " + deviceName);
    throwOnError(snd_async_add_pcm_handler(&m_pAlsaCallBack, m_pAudioDevice, alsaCallBack, this), "Error adding callback");
}

AlsaRenderer::~AlsaRenderer()
{
    if (m_pAudioDevice)
    {
        stop();
        snd_pcm_close(m_pAudioDevice);
    }
}

void AlsaRenderer::setFormat(const AudioFormat& format)
{
    snd_pcm_hw_params_t* pHardwareParam;
    snd_pcm_hw_params_malloc(&pHardwareParam);

    throwOnError(snd_pcm_hw_params_any(m_pAudioDevice, pHardwareParam), "Error setting hwparamsAny");
    throwOnError(snd_pcm_hw_params_set_access(m_pAudioDevice, pHardwareParam, SND_PCM_ACCESS_RW_INTERLEAVED), "Error setting access mode");

    snd_pcm_format_t formatType;
    switch (format.bits)
    {
    case 32:
        formatType = SND_PCM_FORMAT_S32_LE;
        m_FrameSize = format.numChannels == 1 ? 4 : 8;
        break;
    case 24:
        formatType = SND_PCM_FORMAT_S24_LE;
        m_FrameSize = format.numChannels == 1 ? 3 : 6;
        break;
    case 16:
        formatType = SND_PCM_FORMAT_S16_LE;
        m_FrameSize = format.numChannels == 1 ? 2 : 4;
        break;
    default:
        throw logic_error("AlsaRenderer: unsupported format");
    }

    m_FrameSize = snd_pcm_format_physical_width(formatType) / 8;
    m_FrameSize *= format.numChannels;

    throwOnError(snd_pcm_hw_params_set_format(m_pAudioDevice, pHardwareParam, formatType), "Error setting format");

    unsigned int exactRate = format.rate;
    throwOnError(snd_pcm_hw_params_set_rate_near(m_pAudioDevice, pHardwareParam, &exactRate, 0), "Error setting rate");

    if (format.rate != exactRate)
    {
        cout << "Sample rate " << format.rate << " Hz is not supported by your hardware." << endl
             << "Using " << exactRate << " Hz in stead." << endl;
    }

    throwOnError(snd_pcm_hw_params_set_channels(m_pAudioDevice, pHardwareParam, format.numChannels), "Error setting channels");
    throwOnError(snd_pcm_hw_params_set_periods(m_pAudioDevice, pHardwareParam, 2, 0), "Error setting number of periods");

    m_FramesPerPacket = format.framesPerPacket;
    m_BufferSize = format.framesPerPacket * 10;

    throwOnError(snd_pcm_hw_params_set_buffer_size_near(m_pAudioDevice, pHardwareParam, &m_BufferSize), "Error setting buffer size");
    throwOnError(snd_pcm_hw_params_set_period_size_near(m_pAudioDevice, pHardwareParam, &m_PeriodSize, NULL), "Error setting period size");
    throwOnError(snd_pcm_hw_params(m_pAudioDevice, pHardwareParam), "Error setting hardware parameters");
    snd_pcm_hw_params_free(pHardwareParam);

    snd_pcm_sw_params_t* pSoftwareParams;
    snd_pcm_sw_params_malloc(&pSoftwareParams);

    throwOnError(snd_pcm_sw_params_current(m_pAudioDevice, pSoftwareParams), "Error getting software params");
    throwOnError(snd_pcm_sw_params_set_start_threshold(m_pAudioDevice, pSoftwareParams, m_BufferSize - m_PeriodSize), "Error setting start treshold");
    throwOnError(snd_pcm_sw_params_set_avail_min(m_pAudioDevice, pSoftwareParams, m_PeriodSize), "Error setting available minimum");
    throwOnError(snd_pcm_sw_params(m_pAudioDevice, pSoftwareParams), "Error setting software params");

    snd_pcm_sw_params_free(pSoftwareParams);

    throwOnError(snd_pcm_prepare(m_pAudioDevice), "Failed to prepare device");
    vector<byte> buffer(m_PeriodSize * m_FrameSize, 0);
    throwOnError(snd_pcm_writei(m_pAudioDevice, &buffer[0], m_PeriodSize), "Error writing initial silence data");
}

void AlsaRenderer::playFrameData()
{
    snd_pcm_sframes_t avail = snd_pcm_avail_update(m_pAudioDevice);
    while (((snd_pcm_uframes_t)avail > m_PeriodSize) && !m_Frames.empty())
    {
        AlsaFrame* pCurFrame = m_Frames.front();
        //cout << "Available: " << avail << " - " << pCurFrame->m_TotalBytes - pCurFrame->m_ProcessedBytes << ":" << pCurFrame->m_TotalBytes << endl;

        snd_pcm_uframes_t bytesToSend = min((pCurFrame->m_TotalBytes - pCurFrame->m_ProcessedBytes), static_cast<int>(m_PeriodSize * m_FrameSize));
        snd_pcm_sframes_t status = snd_pcm_writei(m_pAudioDevice, pCurFrame->m_pFrameData + pCurFrame->m_ProcessedBytes, bytesToSend / m_FrameSize);

        if (status < 0)
        {
            cerr << "AlsaCallback: Failed to write frame data: " << snd_strerror(status) << endl;
            snd_pcm_prepare(m_pAudioDevice);
            usleep(10000);
        }
        else
        {
            pCurFrame->m_ProcessedBytes += status * m_FrameSize;
            //cout << "Processed: " << status << endl;
        }

        if (pCurFrame->m_ProcessedBytes == pCurFrame->m_TotalBytes)
        {
            delete pCurFrame;
            m_Frames.pop();
            if (!m_Frames.empty())
            {
                m_LastPts = m_Frames.front()->m_Pts;
            }
        }
        else if (pCurFrame->m_ProcessedBytes > pCurFrame->m_TotalBytes)
        {
            cout << "Prutserke!" << endl;
            //exit(0);
        }
        avail = snd_pcm_avail_update(m_pAudioDevice);
    }
}

void AlsaRenderer::play()
{
    //throwOnError(snd_pcm_pause(m_pAudioDevice, 0), "Error starting playback");
}

void AlsaRenderer::pause()
{
    throwOnError(snd_pcm_pause(m_pAudioDevice, 1), "Error pausing playback");
}

void AlsaRenderer::stop()
{
    throwOnError(snd_pcm_drop(m_pAudioDevice), "Error stopping playback");
    clearBuffers();
}

void AlsaRenderer::adjustVolume(float offset)
{

}

bool AlsaRenderer::hasBufferSpace()
{
    return (m_Frames.size() < FRAME_BUFFER_SIZE);
}

void AlsaRenderer::flushBuffers()
{
}

void AlsaRenderer::clearBuffers()
{
    while (!m_Frames.empty())
    {
        delete m_Frames.front();
        m_Frames.pop();
    }
    m_LastPts = 0.0;
}

void AlsaRenderer::queueFrame(const AudioFrame& frame)
{
    if (m_FrameSize == 0)
    {
        throw logic_error("Alsarenderer: Audio format was never set");
    }

    //cout << "QueueFrame: " << m_Frames.size() << endl;
    assert(frame.getFrameData());
    AlsaFrame* pFrame = new AlsaFrame(frame.getFrameData(), frame.getDataSize(), frame.getPts());
    m_Frames.push(pFrame);
    m_LastPts = frame.getPts();

    //playFrameData();
    //throwOnError(snd_async_add_pcm_handler(&m_pAlsaCallBack, m_pAudioDevice, alsaCallBack, this), "Error adding callback");
}

double AlsaRenderer::getCurrentPts()
{
    return m_LastPts;
}

void AlsaRenderer::throwOnError(int err, const string& message)
{
    if (err < 0)
    {
        throw logic_error("AlsaRenderer: " + message + ": " + string(snd_strerror(err)));
    }
}
